package com.rcpcompany.ui.flows;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.progress.IWorkbenchSiteProgressService;

import com.rcpcompany.ui.flows.IFlowDirector.WhereTo;
import com.rcpcompany.utils.logging.LogUtils;

/**
 * The view part for the flow view.
 * <p>
 * Use this class directly in your <code>org.eclipse.ui.views</code> extension
 * point.
 * 
 * @author Tonny Madsen, The RCP Company
 * 
 */
public class FlowView extends ViewPart implements IFlowContainer {

	/**
	 * The map with directors managed by this viewer.
	 */
	final protected Map<IFlowDirector, DirectorInfo> myOpenDirectors = new HashMap<IFlowDirector, DirectorInfo>();

	/**
	 * List of listeners for this flow container
	 */
	final protected List<IFlowContainerListener> myContainerListeners = new ArrayList<IFlowContainerListener>();

	/**
	 * Class that holds all the relevant information for one director.
	 */
	private class DirectorInfo {
		/**
		 * The director itself.
		 */
		public IFlowDirector myDirector;

		/**
		 * <code>true</code> if the director needs to navigate to the first
		 * page.
		 */
		public boolean myNavigateFirstNeeded = true;

		/**
		 * Serial indicating the order to more recently opened flows...
		 */
		public int myLastOpened;

		/**
		 * The composite the holds this director.
		 * <p>
		 * A direct child of {@link FlowView#myTop}.
		 */
		public Composite myComposite;

		/**
		 * The composite of this director that will hold the pages of the
		 * director.
		 * <p>
		 * Child of {@link #myComposite}.
		 */
		public Composite myPagesComposite;

		/**
		 * The {@link StackLayout} used for the pages composite.
		 */
		public StackLayout myPagesCompositeLayout;

		/**
		 * A map with pages managed by this container.
		 */
		public Map<IFlowPage, PageInfo> myPages = new HashMap<IFlowPage, PageInfo>();

		/**
		 * Composite that holds the buttons of the director.
		 */
		public Composite myButtonsComposite;

		/**
		 * A map of standard buttons...
		 */
		public Map<IFlowDirector.WhereTo, Button> myButtons = new HashMap<WhereTo, Button>();
	}

	/**
	 * Class that holds all relevant information for one page.
	 */
	private class PageInfo {
		/**
		 * The page itself.
		 */
		public AbstractFlowPage myPage;

		/**
		 * The Composite that holds this page.
		 */
		public Composite myComposite;
	}

	/**
	 * The current director...
	 */
	private DirectorInfo myOpenDirector;

	/**
	 * The current page...
	 */
	private PageInfo myCurrentPage;

	/**
	 * Serial used for the {@link DirectorInfo#myLastOpened} field.
	 */
	private int lastOpenedSerial = 0;

	/**
	 * Constructs and returns a new view.
	 */
	public FlowView() {
	}

	/**
	 * Opens a specific page of the viewer.
	 * <p>
	 * Uses an empty page if no specific page is specified.
	 * <p>
	 * If page is <em>not</em> already be initialized, then it is so with the
	 * current director.
	 */
	private void openPage() {
		if (myOpenDirector == null)
			return;
		/*
		 * Do everything in the UI thread so we can manipulate the widgets as
		 * well...
		 */
		myTop.getDisplay().asyncExec(new Runnable() {
			public void run() {
				/*
				 * Redo the test...
				 */
				if (myOpenDirector == null)
					return;
				// Make sure any old page is saved first
				if (myCurrentPage != null) {
					myCurrentPage.myPage.saveState();
				}

				final AbstractFlowPage page = (AbstractFlowPage) myOpenDirector.myDirector.getCurrentPage();

				// Has this page been initialized?
				if (page.getFlowDirector() == null) {
					LogUtils.debug(FlowView.this, "New page " + page + " (" + page.getTitle() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					final PageInfo pi = new PageInfo();
					pi.myPage = page;

					pi.myComposite = new Composite(myOpenDirector.myPagesComposite, SWT.NONE);
					pi.myComposite.setLayout(new FillLayout());

					myOpenDirector.myPages.put(page, pi);

					SafeRunner.run(new ISafeRunnable() {
						public void run() throws Exception {
							page.initialize(myOpenDirector.myDirector, FlowView.this);
							page.createControl(pi.myComposite);
						}

						public void handleException(Throwable exception) {
							// TODO TM: closePage(page);
						}
					});
					// TODO TM test page is still open
				}

				LogUtils.debug(FlowView.this, "Opening page " + page + " (" + page.getTitle() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				final PageInfo pi = myOpenDirector.myPages.get(page);
				Assert.isNotNull(pi, "Internal Error: pi == null"); //$NON-NLS-1$

				LogUtils.debug(FlowView.this, "Current page " + pi + " = " + page); //$NON-NLS-1$ //$NON-NLS-2$
				myCurrentPage = pi;
				SafeRunner.run(new ISafeRunnable() {
					public void run() throws Exception {
						pi.myPage.refreshState();
					}

					public void handleException(Throwable exception) {
						// Ignore
					}
				});
				myOpenDirector.myPagesCompositeLayout.topControl = pi.myComposite;
				myOpenDirector.myPagesComposite.layout();
				setTopComposite(myOpenDirector.myComposite);
				Control focusControl = pi.myComposite.getDisplay().getFocusControl();
				while (focusControl != null && focusControl != pi.myComposite) {
					focusControl = focusControl.getParent();
				}
				if (focusControl != pi.myComposite) {
					pi.myComposite.setFocus();
				}

				// Update director title and icon
				setPartName(myOpenDirector.myDirector.getTitle());
				setTitleImage(myOpenDirector.myDirector.getImage());

				updateState();
			}

		});
	}

	/**
	 * Sets the top composite of the view and updates the view..
	 * 
	 * @param top
	 *            the new top composite
	 */
	private void setTopComposite(Composite top) {
		myTopLayout.topControl = top;
		myTop.layout();
	}

	/**
	 * Closes the specific page.
	 * 
	 * @param page
	 *            the page to close
	 */
	private void closePage(final AbstractFlowPage page) {
		LogUtils.debug(FlowView.this, "Close page " + page + " (" + page.getTitle() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		Assert.isTrue(page != null && page.getFlowContainer() == this);

		// Look up the director and page info
		IFlowDirector d = page.getFlowDirector();
		if (d == null) {
			// TODO TM error: page not initialized yet?
			return;
		}

		final DirectorInfo di = myOpenDirectors.get(d);
		if (di == null) {
			// TODO TM error?
			return;
		}
		final PageInfo pi = di.myPages.get(page);
		if (pi == null) {
			// TODO TM error?
			return;
		}
		SafeRunner.run(new ISafeRunnable() {
			public void run() throws Exception {
				pi.myPage.dispose();
			}

			public void handleException(Throwable exception) {
				// Ignore
			}
		});
		pi.myComposite.dispose();
		di.myPages.remove(page);

		if (myCurrentPage == pi) {
			// TODO TM circular refs?
			openPage();
		}
	}

	@Override
	public void dispose() {
		LogUtils.debug(FlowView.this, "Dispose View"); //$NON-NLS-1$
		closeAllOpenDirectors();
		super.dispose();
	}

	public void closeAllOpenDirectors() {
		for (final IFlowContainerListener listener : myContainerListeners) {
			SafeRunner.run(new ISafeRunnable() {
				public void run() throws Exception {
					listener.directorAllClosed(false);
				}

				public void handleException(Throwable exception) {
				}
			});
		}
		while (!myOpenDirectors.isEmpty()) {
			IFlowDirector director = myOpenDirectors.keySet().iterator().next();
			closeDirector(director);
		}
		for (final IFlowContainerListener listener : myContainerListeners) {
			SafeRunner.run(new ISafeRunnable() {
				public void run() throws Exception {
					listener.directorAllClosed(true);
				}

				public void handleException(Throwable exception) {
				}
			});
		}
	}

	@Override
	public void createPartControl(Composite parent) {
		LogUtils.debug(FlowView.this, "Create View"); //$NON-NLS-1$
		myTop = new Composite(parent, SWT.NONE);
		myTop.addDisposeListener(new DisposeListener() {
			public void widgetDisposed(DisposeEvent e) {
				closeAllOpenDirectors();
			}
		});
		myTopLayout = new StackLayout();
		myTop.setLayout(myTopLayout);

		myErrorPage = new Composite(myTop, SWT.NONE);
		myErrorPage.setLayout(new GridLayout());

		final Label noActiveFlowLabel_1 = new Label(myErrorPage, SWT.NONE);

		noActiveFlowLabel_1.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));
		noActiveFlowLabel_1.setText("Error Occured"); //$NON-NLS-1$

		myEmptyPage = new Composite(myTop, SWT.NONE);
		myEmptyPage.setLayout(new GridLayout());

		Label noActiveFlowLabel;
		noActiveFlowLabel = new Label(myEmptyPage, SWT.NONE);
		noActiveFlowLabel.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));
		noActiveFlowLabel.setText("No active flow"); //$NON-NLS-1$
		myTopLayout.topControl = myErrorPage;

		setTopComposite(myEmptyPage);
	}

	@Override
	public void setFocus() {
	}

	/**
	 * Opens a specific flow director to use with this flow viewer.
	 * 
	 * @param director
	 *            the new director
	 */
	public void openDirector(final IFlowDirector director) {
		LogUtils.debug(FlowView.this, "Open director " + director + " (" + director.getTitle() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		Assert.isNotNull(director);
		DirectorInfo di = myOpenDirectors.get(director);

		if (di == null) {
			final IFlowContainer container = FlowView.this;
			di = new DirectorInfo();
			di.myDirector = director;
			di.myComposite = new Composite(myTop, SWT.NONE);
			di.myComposite.setLayout(new GridLayout());
			di.myPagesComposite = new Composite(di.myComposite, SWT.NONE);
			di.myPagesComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
			di.myPagesCompositeLayout = new StackLayout();
			di.myPagesComposite.setLayout(di.myPagesCompositeLayout);
			di.myButtonsComposite = new Composite(di.myComposite, SWT.NONE);
			di.myButtonsComposite.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false));
			di.myButtonsComposite.setLayout(new RowLayout());

			myOpenDirectors.put(director, di);

			myOpenDirector = di;
			// Create standard buttons
			addStandardButton(Messages.FlowView_Previous, WhereTo.PREVIOUS_STEP);
			addStandardButton(Messages.FlowView_Next, WhereTo.NEXT_STEP); 
			addStandardButton(Messages.FlowView_Finish, WhereTo.FINISH);
			addStandardButton(Messages.FlowView_Cancel, WhereTo.CANCEL);

			SafeRunner.run(new ISafeRunnable() {
				public void run() throws Exception {
					director.open(container);
				}

				public void handleException(Throwable exception) {
					closeDirector(director);
				}
			});

			// See if the director was closed :-(
			if (!myOpenDirectors.containsKey(director)) {
				return;
			}

			for (final IFlowContainerListener listener : myContainerListeners) {
				SafeRunner.run(new ISafeRunnable() {
					public void run() throws Exception {
						listener.directorOpened(director);
					}

					public void handleException(Throwable exception) {
					}
				});
			}
		}

		myOpenDirector = di;
		di.myLastOpened = lastOpenedSerial++;
		for (final IFlowContainerListener listener : myContainerListeners) {
			SafeRunner.run(new ISafeRunnable() {
				public void run() throws Exception {
					listener.directorActivated(director);
				}

				public void handleException(Throwable exception) {
				}
			});
		}
		if (myOpenDirector.myNavigateFirstNeeded) {
			myOpenDirector.myNavigateFirstNeeded = false;
			navigate(IFlowDirector.WhereTo.FIRST);
		}
		openPage();
		displayView();
	}

	private void displayView() {
		getSite().getPage().activate(this);
	}

	/**
	 * Navigates the current director according to the direction.
	 * 
	 * @param where
	 *            the direction
	 */
	private void navigate(WhereTo where) {
		if (myOpenDirector == null)
			return;
		LogUtils.debug(FlowView.this, "Navigate " + where); //$NON-NLS-1$

		myOpenDirector.myDirector.navigate(where);
	}

	/**
	 * Closes a specific flow director to use with this flow viewer.
	 * 
	 * @param director
	 *            the new director
	 */
	public void closeDirector(final IFlowDirector director) {
		LogUtils.debug(FlowView.this, "Close director " + director + " (" + director.getTitle() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		final DirectorInfo di = myOpenDirectors.get(director);
		if (di == null)
			return;

		if (di == myOpenDirector) {
			myOpenDirector = null;
		}

		while (!di.myPages.isEmpty()) {
			PageInfo pi = di.myPages.values().iterator().next();
			closePage(pi.myPage);
		}

		SafeRunner.run(new ISafeRunnable() {
			public void run() throws Exception {
				di.myDirector.dispose();
			}

			public void handleException(Throwable exception) {
				// Ignore
			}
		});
		di.myComposite.dispose();
		myOpenDirectors.remove(director);

		for (final IFlowContainerListener listener : myContainerListeners) {
			SafeRunner.run(new ISafeRunnable() {
				public void run() throws Exception {
					listener.directorClosed(director);
				}

				public void handleException(Throwable exception) {
				}
			});
		}

		if (myOpenDirector == null) {
			if (myOpenDirectors.size() == 0) {
				LogUtils.debug(FlowView.this, "Empty page"); //$NON-NLS-1$
				setTopComposite(myEmptyPage);
				for (final IFlowContainerListener listener : myContainerListeners) {
					SafeRunner.run(new ISafeRunnable() {
						public void run() throws Exception {
							listener.directorActivated(null);
						}

						public void handleException(Throwable exception) {
						}
					});
				}
			} else {
				IFlowDirector next = myOpenDirectors.keySet().iterator().next();
				openDirector(next);
			}
		}
	}

	/**
	 * The top level layout...
	 */
	private Composite myTop;

	/**
	 * The stack layout of {@link #myTop}.
	 */
	private StackLayout myTopLayout;

	private Composite myErrorPage;

	private Composite myEmptyPage;

	public Set<IFlowDirector> getOpenDirectors() {
		return Collections.unmodifiableSet(myOpenDirectors.keySet());
	}

	public void pageUpdated(IFlowPage page) {
		updateState();
	}

	/**
	 * Updates the state based on the the current page.
	 */
	private void updateState() {
		if (myOpenDirector != null) {
			for (WhereTo direction : myOpenDirector.myButtons.keySet()) {
				Button button = myOpenDirector.myButtons.get(direction);
				button.setEnabled(myOpenDirector.myDirector.canNavigate(direction));
			}
		}
	}

	public IFlowDirector getCurrentDirector() {
		if (myOpenDirector == null)
			return null;
		return myOpenDirector.myDirector;
	}

	public IFlowPage getCurrentPage() {
		if (myCurrentPage == null)
			return null;
		return myCurrentPage.myPage;
	}

	public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable) throws InvocationTargetException,
			InterruptedException {
		IWorkbenchSiteProgressService ps = (IWorkbenchSiteProgressService) getSite().getService(
				IWorkbenchSiteProgressService.class);
		ps.run(fork, cancelable, runnable);
	}

	public Button addButton(String text) {
		LogUtils.debug(FlowView.this, "addButton " + text + " (" + myOpenDirector.myDirector.getTitle() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		Assert.isNotNull(myOpenDirector);
		Button b = new Button(myOpenDirector.myButtonsComposite, SWT.PUSH);
		b.setText(text);

		return b;
	}

	/**
	 * Adds an standard button to the container. But only if wanted by the
	 * director (see {@link IFlowDirector#isNavigationButtonWanted(WhereTo)}).
	 * 
	 * @param text
	 *            the text of the button
	 * @param direction
	 *            the direction for the button
	 */
	private void addStandardButton(String text, final WhereTo direction) {
		if (!myOpenDirector.myDirector.isNavigationButtonWanted(direction))
			return;
		Button button = addButton(text);

		button.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				navigate(direction);
				openPage();
			}
		});
		myOpenDirector.myButtons.put(direction, button);
	}

	public void addListener(IFlowContainerListener listener) {
		myContainerListeners.add(listener);
	}

	public void removeListener(IFlowContainerListener listener) {
		myContainerListeners.remove(listener);
	}
}
